package org.sqlproc.engine.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The SQL type of a dynamic input value (SQL statement parameter) or an output value (SQL query scalar).
 * 
 * @author <a href="mailto:Vladimir.Hudec@gmail.com">Vladimir Hudec</a>
 */
class SqlType {

    /**
     * The internal type, which means special processing of the input/output value.
     */
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * The internal type, which means special processing of the input/output value.
     */
    private SqlMetaType metaType;
    /**
     * The Hibernate type. A standard way to assing the type of parameter/scalar binding to the Hibernate Query.
     */
    private Type hibernateType;
    /**
     * Right now only for the special of the enumeration type of the input value. The logical evaluation of the input
     * value is based on the comparison to this value.
     */
    private String value;

    /**
     * Creates a new instance with unspecified internal and Hibernate types.
     */
    SqlType() {
        this.metaType = SqlMetaType.DEFAULT;
        this.hibernateType = null;

    }

    /**
     * Creates a new instance with specified internal and Hibernate types.
     * 
     * @param metaType
     *            the internal type
     * @param hibernateType
     *            the Hibernate type
     */
    SqlType(SqlMetaType metaType, Type hibernateType) {
        this.metaType = metaType;
        this.hibernateType = hibernateType;

    }

    /**
     * Creates a new instance with specified Hibernate type.
     * 
     * @param sHibernateType
     *            the String representation of the Hibernate type
     */
    SqlType(String sHibernateType) {
        this.metaType = SqlMetaType.HIBERNATE;
        sHibernateType = sHibernateType.toUpperCase();
        Field f = hibernateTypes.get(sHibernateType);
        if (f != null) {
            try {
                this.hibernateType = (Type) f.get(null);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new RuntimeException("Unsupported Hibernate Type " + sHibernateType);
        }
    }

    /**
     * Creates a new instance with specified internal type.
     * 
     * @param sMetaType
     *            the String representation of the internal type
     * @param previousType
     *            the prevoius value of the input/output value type
     */
    SqlType(String sMetaType, SqlType previousType) {
        this.metaType = SqlMetaType.metaToTypeMap.get(sMetaType.toUpperCase());
        if (this.metaType != null) {
            if (previousType != null && previousType.getMetaType() == SqlMetaType.HIBERNATE)
                this.hibernateType = previousType.getHibernateType();
            else
                this.hibernateType = null;
        } else {
            throw new RuntimeException("Unsupported Meta Type " + sMetaType);
        }
    }

    /**
     * Returns the Hibernate type.
     * 
     * @return the Hibernate type
     */
    Type getHibernateType() {
        return (this.hibernateType != null) ? this.hibernateType : this.metaType.getHibernateType();
    }

    /**
     * Returns the Hibernate type of the attribute in the target pojoClass.
     * 
     * @param pojoClass
     *            the target class
     * @param attributeName
     *            the name of the attribute in the target pojoClass
     * @return the Hibernate type
     */
    Type getHibernateType(Class<?> pojoClass, String attributeName) {
        Class<?> attributeType = SqlUtils.getFieldType(pojoClass, attributeName);
        return SqlType.hibernateTypes2.get(attributeType);
    }

    /**
     * Returns the internal type.
     * 
     * @return the internal type
     */
    SqlMetaType getMetaType() {
        return this.metaType;
    }

    /**
     * Initializes the attribute of the result class with output values from SQL query execution.
     * 
     * @param resultInstance
     *            the instance of the result class
     * @param attributeName
     *            the name of the attribute in the result class
     * @param resultValue
     *            Query execution output value
     */
    void setResult(Object resultInstance, String attributeName, Object resultValue) {
        if (logger.isDebugEnabled())
            logger.debug("setResult " + metaType + " " + attributeName + " " + resultValue);
        metaType.setResult(resultInstance, attributeName, resultValue, getHibernateType());
    }

    /**
     * Bind an input value to a named query parameter.
     * 
     * @param query
     *            the object-oriented representation of a Hibernate query
     * @param paramName
     *            the name of the parameter
     * @param inputValue
     *            the possibly-null parameter value, a dynamic input value
     */
    void setParameter(Query query, String paramName, Object inputValue, Class<?> inputType) {
        if (logger.isDebugEnabled())
            logger.debug("setParameter " + metaType + " " + paramName + " " + inputValue);
        metaType.setParameter(query, paramName, inputValue, inputType, getHibernateType());
    }

    /**
     * Returns the value. Right now only for the special of the enumeration type of the input value. The logical
     * evaluation of the input value is based on the comparison to this value.
     * 
     * @return the value for special enumeration treatment
     */
    public String getValue() {
        return value;
    }

    /**
     * Sets the value. Right now only for the special of the enumeration type of the input value. The logical evaluation
     * of the input value is based on the comparison to this value.
     * 
     * @param value
     *            the value for special enumeration treatment
     */
    public void setValue(String value) {
        this.value = value;
    }

    /**
     * The map between a String representation of Hibernate types and a Hibernate types.
     */
    static Map<String, Field> hibernateTypes = new HashMap<String, Field>();
    static {
        Field[] fields = Hibernate.class.getFields();
        for (Field f : fields) {
            if (!Modifier.isStatic(f.getModifiers()))
                continue;
            try {
                if (f.get(null) instanceof Type)
                    hibernateTypes.put(f.getName().toUpperCase(), f);
            } catch (IllegalArgumentException e) {
                throw e;
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * The map between an input/output Java types and a Hibernate types.
     */
    static Map<Class<?>, Type> hibernateTypes2 = new HashMap<Class<?>, Type>();
    static {
        hibernateTypes2.put(int.class, Hibernate.INTEGER);
        hibernateTypes2.put(Integer.class, Hibernate.INTEGER);
        hibernateTypes2.put(long.class, Hibernate.LONG);
        hibernateTypes2.put(Long.class, Hibernate.LONG);
        hibernateTypes2.put(short.class, Hibernate.SHORT);
        hibernateTypes2.put(Short.class, Hibernate.SHORT);
        hibernateTypes2.put(float.class, Hibernate.FLOAT);
        hibernateTypes2.put(Float.class, Hibernate.FLOAT);
        hibernateTypes2.put(double.class, Hibernate.DOUBLE);
        hibernateTypes2.put(Double.class, Hibernate.DOUBLE);
        hibernateTypes2.put(char.class, Hibernate.CHARACTER);
        hibernateTypes2.put(Character.class, Hibernate.CHARACTER);
        hibernateTypes2.put(String.class, Hibernate.STRING);
        hibernateTypes2.put(java.util.Date.class, Hibernate.TIMESTAMP);
        hibernateTypes2.put(boolean.class, Hibernate.BOOLEAN);
        hibernateTypes2.put(Boolean.class, Hibernate.BOOLEAN);
        hibernateTypes2.put(BigInteger.class, Hibernate.BIG_INTEGER);
        hibernateTypes2.put(BigDecimal.class, Hibernate.BIG_DECIMAL);
        hibernateTypes2.put(byte[].class, Hibernate.BINARY);
        hibernateTypes2.put(Byte[].class, Hibernate.WRAPPER_BINARY);
    }

    /**
     * For debug purposes.
     * 
     * @return a String representation for a debug output
     */
    public String toString() {
        StringBuilder sb = new StringBuilder("Type{");
        sb.append(this.metaType).append(",h=").append(this.hibernateType).append(",v=").append(this.value).append("}");
        return sb.toString();
    }
}
